RenderThread:异步渲染动画
Android中动画是比较常见的,无论你是使用的补间动画还是属性动画,都无法避免对UI的绘制,那么异步渲染是否可行呢?答案当然是可行的,其关键就是RenderThread,对于RenderThread可能有些人对它并不甚了解,下面先简要的介绍下它。
作者:郭海洋
Android 高级开发工程师,主要负责统一控件的开发和维护,以及新技术的研究和创新工作。
看到此处,你可能会有疑问,这里的Open GL线程又是什么呢?要理解此处,首先要明确硬件加速是什么,其实就是通过GPU来渲染。GPU作为一个硬件,应用程序无法直接使用,它是由GPU厂商按照Open GL规范实现的驱动,间接进行使用的。那么当应用程序使用Open GL去渲染UI的时候,Android应用程序的UI就是通过硬件加速来渲染的,也就是说此处的Open GL线程就是处理硬件加速绘制的线程。
在Android3.0的时候,UI的绘制就支持硬件加速了,不过此时默认是关闭的,应该是发展初期并不稳定。当Android4.0之后,UI的绘制默认开启硬件加速,此时支持应该相对完善了,到了Android5.0出现了Render Thread专门处理硬件加速相关的绘制。
通过上面的论述我们可以简单的得到一个结论,在Android5.0之前无论计算还是绘制都是在主线程上的,而之后,对于启用了硬件加速的UI来说,至少绘制可以在Render Thread上进行。这也就是为什么Android 5.0之后动画更加酷炫了,但UI似乎比之前更加流畅的原因。
至此,我们只是简单的了解了RenderThread是什么,但是对于它如何在Android的绘制中发挥作用,又是如何可以异步渲染动画还为提及。为了解决这些疑问,下面我们来探索Android UI的绘制流程。
对于Android的UI绘制,基本上可以分两步进行。第一步是Android的应用程序进程中进行,第二步在SurfaceFlinger进程中进行。第一步的时候,将UI绘制到图形缓存区域中,然后交由第二步合成以及显示到屏幕中。对于Android的UI绘制,区分基于软件的绘制和硬件加速绘制两种,下面先介绍下软件绘制。
软件绘制
在讲解之前,先看图1,对软件绘制有个整体认识。
图1
在基于软件的绘制模式下,CPU主导绘图,视图依照两个步骤绘制UI:
1)让View层次结构失效;
2)绘制View层次结构。
当应用程序需要更新UI时,都会调用内容发生改变的View的invalidate方法,让其失效的请求会在View对象层次结构中传递,以便计算出需要重绘的屏幕区域(脏区)。然后,Android系统会在View层次结构中绘制所有的跟脏区相交的区域。
在Android应用中每个窗口都关联一个Surface,当需要绘制UI的时候,会调用对应的Surface的lockCanvas方法获取一个Canvas,其本质就是通过SurfaceFlinger 服务Dequeue一个GraphicBuffer。绘制完成后,Android应用调用对应Surface的unlockCanvasAndPost方法请求显示在屏幕中,其本质是向SurfaceFlinger服务Dequeue一个GraphicBuffer,以便SurfaceFlinger服务可以对Graphic Buffer的内容进行合成以及显示到屏幕上去。对于View中onDraw方法中的参数Canvas来说,它便是Surface通过lockCanvas方法获取的Canvas。对于软件绘制来说,它也有不可避免的缺陷:
1)绘制了不需要重绘的视图(与脏区相交的区域);
2)掩盖了一些应用缺陷(因为重绘制了内容)。
至此,我们已经了解了Android中基于软件绘制的流程,正因为软件绘制还有许多不完美,才促使了硬件加速绘制的产生,下面我们讲一下基于硬件加速绘制的流程。
硬件加速绘制
图2
对于硬件加速来说,Android 5.0之后由于引入了Render Thread,其绘制过程发生了些许的变化,看图3。
图3
在Android 5.0之前的时候,UI发生变化后,主线程内首先要更新对应View的展示列表,当VSYNC信号到来时候,将对应的展示列表转化为对应的OpenGL命令然后调用OpenGL 的函数完成绘制,也就是说主线程既需要完成展示列表的构建工作,又需要通过展示列表转为OpenGL命令完成绘制工作。在Android 5.0 之后,当UI发生改变后,在主线程中更新对应View的展示列表,在VSYNC信号到来时,向Render Thread发出drawFrame的命令,Render Thread内部有一个Task Queue用于接收绘制命令,接收后等待Render Thread的处理。也就是说Android 5.0之后主线程仍旧负责View的展示列表的更新,但绘制交由Render Thread。
至此,我们已经知晓,RenderThread作为Android 5.0的产物,是当UI开启硬件加速后,用于分担主线程绘制任务的渲染线程。这里我们可以得到两个结论:1)Android5.0之后,UI绘制可以异步绘制;2)绘制可以异步,动画可以异步也似乎成为可能,因为阻碍动画异步的主要原因就是UI绘制。但知道了这些,仍不清楚RenderThread对于动画有什么作用,不过,正因为有了这些知识的积累,才能继续后续的探索,好,带着疑问,下面开始探索Android中动画如何使用硬件加速。
在讲解之前,我们先看下图4。
图4
动画中使用硬件加速有两种方式,一种是通过View Layer,使用Frame Buffer Object的方式绘制,第二种就是通过将动画注册到RenderThread中进行构建和绘制。下面开始分析第一种。
通过View Layer,使用Frame Buffer Object的方式绘制。View Layer又称为离屏缓冲,它的作用就是将绘制的结果进行缓存,缓存的结果可能是OpenGL的纹理或者是Bitmap,取决于硬件加速是否开启。当UI开启硬件加速的时候,开启动画时设置 setLayerType(LAYER_TYPE_HARDWARE,null),并使用buildLayer方法构建View的Layer,并在动画结束后,使用setLayerType(LAYER_TYPE_NONE, null);进行恢复Layer。此时动画的每一帧都可以通过Open GL的Frame Buffer Object的方式绘制。此处的绘制也和上面提及的一样,也只能是Android 5.0后可以使用Render Thread 绘制,否则仍是主线程中绘制,而且并不能完全异步渲染,还需依赖主线程构建UI的显示列表。但只要使用View Layer,无论是否开启硬件加速,绘制时都会对原有内容重用,提升绘制效率。但是只针对View内容不发生变更的操作,也就是View中无需使用 invalidate方法的操作,比如位移、旋转、透明度等操作。此方案能提高动画的绘制效率但并不是我们想要的,继续看下一种。
通过将动画注册到Render Thread中进行构建和绘制:将待执行的动画注册到 Render Thread 中的AnimatorManager 之后,AnimatorManager开始检测动画是否完成结束,如果还没有结束,那么Render Thread就自动地计算和显示动画的下一帧,直到动画显示结束为止。此时Canvas由DisplayListCanvas实现,方法参数类型使用CanvasProperty对象替换原有的基本类型(例如CanvasProperty<Float>替换float)。此时动画的初始化以及初始的显示列表以及对应的绘制操作在UI线程中创建,之后Render Thread可以通过CanvasProperty异步的进行修改,从而使得后续的显示列表的更新以及绘制都可以由RenderThread进行,从而达到异步渲染动画的目的。很明显,我们终于找到了,这个就是我们要找的。
至此我们终于明白了,在开启硬件加速的时候,Android5.0之前没有RenderThread,主线程需要参与构建和绘制,Android5.0之后出现的RenderThread,可以分担主线程的绘制任务,而动画可以注册到RenderThread中,让RenderThread自己完成构建和绘制,不需依赖主线程。但仅仅如此吗?理论还需实践的支撑,下面我们通过代码的探索来印证理论,然后实现RenderThread渲染动画,来为此次探索,画上完美的句号。
经过查阅官方文档,得知目前支持RenderThread完全渲染的动画,只有两种:ViewPropertyAnimator和CircularReveal(揭露动画)。对于CircularReveal(揭露动画)使用比较简单且功能较为单一,在此不做过多的探索,着重讲解下ViewPropertyAnimator。
从源码逆推实现
通过刚才的知识,我们了解到如果要实现RenderThread完全渲染动画,只需要将动画注册到RenderThread的AnimatorManager中即可,其余的系统会处理。由于ViewPropertyAnimator并不是任何情况下都是RenderThread完全渲染的动画,我们尝试从源码中探究ViewPropertyAnimator什么情况下,才能实现RenderThread完全渲染的动画,从源码逆推实现。
首先,我们看看如何使用ViewPropertyAnimator呢?
View view = findViewById(R.id.button);
ViewPropertyAnimator animator = view.animate().scaleX(1).translationX(1).alpha(1);
animator.start();
我们可以看到View调用animate方法就可以创建ViewPropertyAnimator 的动画,它有缩放、位移、透明度相关的方法。但需要注意的是ViewPropertyAnimator 并不是Animator的子类。然后看下ViewPropertyAnimator 的start方法:
public class ViewPropertyAnimator {
......
public void start() {
......
startAnimation();
}
......
}
可以看到,start方法内调用了startAnimation方法:
public class ViewPropertyAnimator {
......
/**
* A RenderThread-driven backend that may intercept startAnimation
*/
private ViewPropertyAnimatorRT mRTBackend;
......
private void startAnimation() {
if (mRTBackend != null && mRTBackend.startAnimation(this)) {
return;
}
......
ValueAnimator animator = ValueAnimator.ofFloat(1.0f);
......
animator.start();
}
......
}
通过代码我们可以看到,startAnimation方法首先对mRTBackend 进行了判断,判断不为null的话,则直接由它执行动画。如果判断失败,则继续执行后续的语句,其中mRTBackend是ViewPropertyAnimatorRT 类型的。很明显判断失败后执行的就是属性动画,此时并不支持RenderThread完全渲染,这也就是不满足的情况。再结合mRTBackend 的注释,很明显RenderThread渲染动画应该和ViewPropertyAnimatorRT相关,我们得到首要条件,mRTBackend不为null。
下面开始探索ViewPropertyAnimatorRT#startAnimation方法来进一步确认:
class ViewPropertyAnimatorRT {
......
public boolean startAnimation(ViewPropertyAnimator parent) {
......
if (!canHandleAnimator(parent)) {
return false;
}
doStartAnimation(parent);
return true;
}
......
}
startAnimation方法先使用canHandleAnimator方法判断,判断返回false直接退出,返回true后,再执行doStartAnimation方法,我们首先看下canHandleAnimator方法:
class ViewPropertyAnimatorRT {
......
private boolean canHandleAnimator(ViewPropertyAnimator parent) {
......
if (parent.getUpdateListener() != null) {
return false;
}
if (parent.getListener() != null) {
// TODO support
return false;
}
if (!mView.isHardwareAccelerated()) {
// TODO handle this maybe?
return false;
}
if (parent.hasActions()) {
return false;
}
// Here goes nothing...
return true;
}
......
}
通过此处的代码可以看到,如果执行动画的View不支持硬件加速,或者动画设置了监听Listener或者UpdateListener,或者动画设置了Action(动画开始结束的监听),都会导致返回false,从而导致doStartAnimation就无法执行。此处我们得到进一步的条件,不进行各种设置,确保 canHandleAnimator返回true。
下面继续看一下doStartAnimation方法:
class ViewPropertyAnimatorRT {
......
private void doStartAnimation(ViewPropertyAnimator parent) {
int size = parent.mPendingAnimations.size();
......
for (int i = 0; i < size; i++) {
NameValuesHolder holder = parent.mPendingAnimations.get(i);
int property = RenderNodeAnimator.mapViewPropertyToRenderProperty(holder.mNameConstant);
final float finalValue = holder.mFromValue + holder.mDeltaValue;
RenderNodeAnimator animator = new RenderNodeAnimator(property, finalValue);
animator.setStartDelay(startDelay);
animator.setDuration(duration);
animator.setInterpolator(interpolator);
animator.setTarget(mView);
animator.start();
mAnimators[property] = animator;
}
parent.mPendingAnimations.clear();
}
......
}
doStartAnimation方法对于每个动画属性都创建了RenderNodeAnimator,然后将对应的动画参数也设置给了RenderNodeAnimator。此处也就完成了动画和属性的绑定。
这样代码就探索到了RenderNodeAnimator,经过查看发现setTarget方法比较重要,下面看下它的代码:
public class RenderNodeAnimator extends Animator {
......
public void setTarget(View view) {
mViewTarget = view;
setTarget(mViewTarget.mRenderNode);
}
private void setTarget(RenderNode node) {
......
mTarget = node;
mTarget.addAnimator(this);
}
......
}
我们可以看到setTarget方法将当前View的RenderNode和RenderNodeAnimator通过addAnimator进行绑定。在RenderNode#addAnimator方法中调用了Native方法nAddAnimator,我们看下其Natvie代码的实现:
static void android_view_RenderNode_addAnimator(JNIEnv* env, jobject clazz,
jlong renderNodePtr, jlong animatorPtr) {
RenderNode* renderNode = reinterpret_cast<RenderNode*>(renderNodePtr);
RenderPropertyAnimator* animator = reinterpret_cast<RenderPropertyAnimator*>(animatorPtr);
renderNode->addAnimator(animator);
}
void RenderNode::addAnimator(const sp<BaseRenderNodeAnimator>& animator) {
mAnimatorManager.addAnimator(animator);
}
至此,我们了解了动画如何被添加到AnimatorManager中。根据刚才的知识,后续AnimatorManager和RenderThread的操作交由系统去处理,进而让RenderThread去完全管理动画,实现RenderThread渲染动画。当然这不是探索的终点,对于RenderThread同AnimatorManager如何交互,又是如何绘制,我们并没有详细的叙述,这不是探索的重点,在此不再细究,如有兴趣可自行继续深入。
从原理到代码实践
我们通过ViewPropertyAnimatorRT#canHandleAnimator方法知道,为了使其方法返回True,一定要满足,不设置各种回调,且View支持硬件加速。
我们可以看到ViewPropertyAnimatorRT 是让动画交由RenderThread处理的关键,但是翻阅源码,并未发现何处创建了这个对象,为了能达到预期的效果,我们自己进行设置。通过查看源码,此类是属于包保护级别的,不是hide的类,在Android P 即使使用反射并不会有影响,下面看下代码实现:
创建对应的view的ViewPropertyAnimatorRT 。
/**
* 创建对应的View的ViewPropertyAnimatorRT
*/
private static Object createViewPropertyAnimatorRT(View view) {
try { Class<?> animRtClazz = Class.forName("android.view.ViewPropertyAnimatorRT");
Constructor<?> animRtConstructor = animRtClazz.getDeclaredConstructor(View.class);
animRtConstructor.setAccessible(true);
Object animRt = animRtConstructor.newInstance(view); return animRt;
} catch (Exception e) { Log.d(TAG, "创建ViewPropertyAnimatorRT出错,错误信息:" + e.toString()); return null;
}
}
然后为viewpropertyAnimator设置对应的mRTBackend的值。
private static void setViewPropertyAnimatorRT(ViewPropertyAnimator animator, Object rt) { try {
Class<?> animClazz = Class.forName("android.view.ViewPropertyAnimator");
Field animRtField = animClazz.getDeclaredField("mRTBackend");
animRtField.setAccessible(true);
animRtField.set(animator, rt);
} catch (Exception e) {
Log.d(TAG, "设置ViewPropertyAnimatorRT出错,错误信息:" + e.toString());
}
}
在动画执行前需要执行的方法。
/**
* 在执行动画开始前配置
*/
public static void onStartBeforeConfig(ViewPropertyAnimator animator, View view) {
Object rt = createViewPropertyAnimatorRT(view);
setViewPropertyAnimatorRT(animator, rt);
}
我们看下使用RenderThread渲染动画的完整代码。
private void startAnim2(View v) {
v.setScaleY(1);
ViewPropertyAnimator animator = v.animate().scaleY(2).setDuration(2000);
AnimHelper.onStartBeforeConfig(animator, v);
animator.start();
}
为了达到测试的效果,我们在执行动画期间使用Thread.sleep模拟线程卡顿。完整的RenderThread渲染动画的测试代码如下:
findViewById(R.id.button2).setOnClickListener(new View.OnClickListener() { @Override
public void onClick(View v) {
startAnim2(v);
delaySleep(1000, 3000);
}
});
我们在动画执行期间模拟UI线程卡顿的效果。
private void delaySleep(long delay, final long duration) { handler.postDelayed(new Runnable() { @Override
public void run() { try {
Thread.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, delay);
}
为了对比实现效果,另一种实现:
private void startAnim1(View v) {
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(v, "scaleY", 1, 2);
objectAnimator.setDuration(2000);
objectAnimator.start();
}
另一种实现的完整代码。
//属性动画
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() { @Override
public void onClick(View v) {
startAnim1(v);
delaySleep(1000, 3000);
}
});
此时运行代码,执行不同的动画,就会看到不同的效果,具体效果可参考此GIF效果,如图5。
图5
可以很明显的看到一般的动画,当主线程阻塞的时候,会出现明显的丢帧卡顿,而使用RenderThread渲染的动画即使阻塞了主线程仍不受影响。
至此,已经完成对RenderThread异步渲染动画的探索,如有不同观点,还请留言指正。
长按识别图中二维码立即关注